package com.gitee.qdbp.jdbc.plugins.impl;

import java.math.BigDecimal;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.jdbc.core.SqlParameterValue;
import com.gitee.qdbp.able.exception.ServiceException;
import com.gitee.qdbp.able.jdbc.model.DbFieldName;
import com.gitee.qdbp.able.jdbc.model.DbFieldValue;
import com.gitee.qdbp.able.jdbc.model.DbRawValue;
import com.gitee.qdbp.jdbc.exception.DbErrorCode;
import com.gitee.qdbp.jdbc.model.DbVersion;
import com.gitee.qdbp.jdbc.model.SimpleFieldColumn;
import com.gitee.qdbp.jdbc.model.TypedDbVariable;
import com.gitee.qdbp.jdbc.model.ValidStrategy;
import com.gitee.qdbp.jdbc.plugins.ColumnValueValidator;
import com.gitee.qdbp.jdbc.plugins.DbPluginHelper;
import com.gitee.qdbp.jdbc.plugins.SqlDialect;
import com.gitee.qdbp.jdbc.sql.SqlTools;
import com.gitee.qdbp.tools.utils.StringTools;

/**
 * 字段值校验处理类 (只在crudDao/crudStream的新增/修改/批量新增/批量修改时有效)<br>
 * <b>对于数字:</b><br>
 * 数字转换为BigDecimal, 取precision与列的columnPrecision, 取scale与列的columnScale对比<br>
 * 如果小数位大于指定值, 先四舍五入到指定小数位; 再判断整体精度是否大于指定精度<br>
 * 超过精度有4种处理策略: 不处理|警告|抛出异常|转换(返回最大精度的999...)<br>
 * <br>
 * <b>对于字符串:</b><br>
 * 根据UTF8编码规则将字符分为ABCD4种类型, 分别确定数据库中所占长度, 与列的columnLength比对<br>
 * 超过长度有4种处理策略: 不处理|警告|抛出异常|转换(截短)<br>
 * 可使用'A','ð','壹','😎'作为4种类型的代表来测试数据库存储实际占用长度<br>
 * 测试用例: CharLengthTest.java<br>
 * <pre>
 * TYPE | UNICODE (HEX)         | UTF-8 (BYTE)
 *  A   | 0000 0000 ~ 0000 007F | 0xxxxxxx
 *  B   | 0000 0080 ~ 0000 07FF | 110xxxxx 10xxxxxx
 *  C   | 0000 0800 ~ 0000 FFFF | 1110xxxx 10xxxxxx 10xxxxxx
 *  D   | 0001 0000 ~ 0010 FFFF | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
 * -----------------------------------------------------------
 * 收集到的部分数据库字符长度情况:
 * MySQL  UTF8    : 一个ascii/一个非ascii/emoji保存会报错
 * MySQL  UTF8MB4 : 一个ascii/一个非ascii/一个emoji, 都占一个宽度
 * DB2    GBK     : 一个ascii占一个宽度/一个非ascii占两个宽度/一个emoji也占两个宽度(emoji不能正常保存)
 * ORACLE GBK     : 一个ascii占一个宽度/一个非ascii在GBK下占两个宽度,在UTF8下占三个宽度/一个emoji占四个宽度
 * </pre>
 *
 * @author zhaohuihua
 * @version 20210801
 */
public class SimpleColumnValueValidator implements ColumnValueValidator, DbPluginHelper.Aware {

    private static final Logger log = LoggerFactory.getLogger(SimpleColumnValueValidator.class);

    /** 默认验证策略, 注意: 由于@Column的length/precision/scale有默认值, 一旦启用, 则所有字段都需要覆盖设置! **/
    protected ValidStrategy defaultValidStrategy = ValidStrategy.NONE;
    /** 字符长度映射表: key=charType(ABCD), value=DbLength **/
    protected final Map<Character, Integer> defaultCharLengthMaps;
    /** 数据库插件帮助类 **/
    protected DbPluginHelper plugins;

    public SimpleColumnValueValidator() {
        Map<Character, Integer> charLengthMaps = new HashMap<>();
        charLengthMaps.put('A', 1);
        charLengthMaps.put('B', 2);
        charLengthMaps.put('C', 3);
        charLengthMaps.put('D', 4);
        this.defaultCharLengthMaps = Collections.unmodifiableMap(charLengthMaps);
    }

    protected Map<Character, Integer> getCharLengthMaps(String tableName, SimpleFieldColumn column,
            SqlDialect dialect) {
        return defaultCharLengthMaps;
    }

    /**
     * 解析字符长度映射表<br>
     * key=charType(ABCD), value=DbLength<br>
     * 例如, MySQL UTF8MB4, 一个ascii/一个非ascii/一个emoji, 都占一个宽度<br>
     * -- 配置为 A:1; B:1; C:1; D:1<br>
     * 例如, DB2 GBK, 一个ascii占一个宽度/一个非ascii占两个宽度/一个emoji也占两个宽度(emoji不能正常保存)<br>
     * -- 配置为 A:1; B:2; C:2; D:2<br>
     * 
     * @param config 字符长度映射
     * @return 字符长度映射表
     */
    protected Map<Character, Integer> parseCharLengthMaps(String config) {
        if (config == null || config.length() == 0) {
            return defaultCharLengthMaps;
        }
        Map<Character, Integer> charLengthMaps = new HashMap<>(defaultCharLengthMaps);
        String[] array = StringTools.split(config, ';');
        for (int i = 0, z = array.length; i < z; i++) {
            String item = array[i];
            int index = item.indexOf(':');
            if (index < 0) {
                if (StringTools.isDigit(item)) {
                    int number = Integer.parseInt(item);
                    charLengthMaps.put((char) ('A' + i), number);
                }
            } else if (index > 0) {
                String key = item.substring(0, index).trim();
                String value = item.substring(index + 1).trim();
                if (key.length() == 1 && value.length() == 1 && StringTools.isDigit(value)) {
                    charLengthMaps.put(key.charAt(0), Integer.parseInt(value));
                }
            }
        }
        return charLengthMaps;
    }

    @Override
    public Object valid(String tableName, SimpleFieldColumn column, Object fieldValue, ValidStrategy validStrategy,
            SqlDialect dialect) {
        // AUTO, 根据默认配置确定实际值
        if (validStrategy == ValidStrategy.AUTO) {
            validStrategy = defaultValidStrategy;
        }
        // 如果校验策略为空或NONE则不校验
        if (validStrategy == null || validStrategy == ValidStrategy.NONE) {
            return fieldValue;
        }
        if (fieldValue instanceof DbRawValue) {
            // 原生值(sysdate, CURRENT_TIMESTAMP等), 不需要校验
            return fieldValue;
        } else if (fieldValue instanceof DbFieldName) {
            // 字段名, 不需要校验
            return fieldValue;
        }

        Object original = SqlTools.unwrapDbVariable(fieldValue);
        Object result = original;
        result = validStringLength(tableName, column, result, validStrategy, dialect);
        result = validNumberPrecision(tableName, column, result, validStrategy, dialect);

        // 这里是比较内存地址而不是equals
        if (result == original) {
            // 相等说明没变化, 返回原值
            return fieldValue;
        } else {
            if (fieldValue instanceof TypedDbVariable) {
                TypedDbVariable temp = ((TypedDbVariable) fieldValue);
                return new TypedDbVariable(temp.getJdbcType(), result);
            } else if (fieldValue instanceof SqlParameterValue) {
                SqlParameterValue temp = ((SqlParameterValue) fieldValue);
                return new SqlParameterValue(temp, result);
            } else if (fieldValue instanceof DbFieldValue) {
                return new DbFieldValue(result);
            } else {
                return result;
            }
        }
    }

    /** 验证数字 **/
    protected Object validNumberPrecision(String tableName, SimpleFieldColumn column, Object fieldValue,
            ValidStrategy validStrategy, SqlDialect dialect) {
        if (!(fieldValue instanceof Number)) {
            return fieldValue;
        }
        Integer maxPrecision = column.getColumnPrecision();
        Integer maxScale = column.getColumnScale();
        if (maxPrecision == null || maxPrecision <= 0) {
            return fieldValue;
        }

        // 如果小数位大于指定值, 先四舍五入到指定小数位; 再判断整体精度是否大于指定精度
        BigDecimal number;
        if (fieldValue instanceof BigDecimal) {
            number = (BigDecimal) fieldValue;
        } else {
            number = new BigDecimal(fieldValue.toString()).stripTrailingZeros();
        }
        // new BigDecimal(20000).stripTrailingZeros(); = 2E+4 (科学计数法)
        // 此时precision=1,scale=-4; 需要将scale设置为0
        if (number.scale() < 0) {
            number = number.setScale(0, BigDecimal.ROUND_HALF_UP);
        }
        if (maxScale == null || maxScale < 0) {
            maxScale = 0;
        }
        if (number.scale() > maxScale) {
            number = number.setScale(maxScale, BigDecimal.ROUND_HALF_UP);
        }
        if (number.precision() <= maxPrecision) {
            return fieldValue;
        }

        String details = String.format(
            "Value '%s' greater than the specified precision for %s.%s, actual precision: (%s,%s), max precision: (%s,%s)",
            fieldValue, tableName, column.getColumnName(), number.precision(), number.scale(), maxPrecision, maxScale);
        if (validStrategy == ValidStrategy.THROW) { // 抛出异常
            throw new ServiceException(DbErrorCode.DB_COLUMN_VALUE_TOO_LONG, details);
        } else if (validStrategy == ValidStrategy.TRANSFORM) { // 截短
            BigDecimal truncatedValue = truncateNumber(tableName, column, maxPrecision, maxScale, number, dialect);
            log.info(details + ", truncated value: " + truncatedValue);
            return truncatedValue;
        } else { // 警告
            log.warn(details);
            return fieldValue;
        }
    }

    /** 截短数字 (返回最大精度的999...) **/
    protected BigDecimal truncateNumber(String tableName, SimpleFieldColumn column, int precision, int scale,
            BigDecimal number, SqlDialect dialect) {
        StringBuilder buffer = new StringBuilder();
        // 整数部分
        int integral = precision - scale;
        if (integral == 0) {
            buffer.append('0');
        } else {
            for (int i = 0; i < integral; i++) {
                buffer.append('9');
            }
        }
        // 小数部分
        if (scale > 0) {
            buffer.append('.');
            for (int i = 0; i < scale; i++) {
                buffer.append('9');
            }
        }
        return new BigDecimal(buffer.toString());
    }

    /** 验证字符串 **/
    protected Object validStringLength(String tableName, SimpleFieldColumn column, Object fieldValue,
            ValidStrategy validStrategy, SqlDialect dialect) {
        Integer maxLength = column.getColumnLength();
        if (maxLength == null || maxLength <= 0 || !(fieldValue instanceof CharSequence)) {
            return fieldValue;
        }
        DbVersion dbVersion = dialect.dbVersion();
        String stringValue = fieldValue.toString();
        int actualLength;
        // 字段定义上有写NVARCHAR, 且当前数据库支持NVARCHAR
        if (column.isSupportUnicode() && plugins.configSupports("supports.unicode", dbVersion)) {
            actualLength = countUnicodeLength(tableName, column, stringValue, dialect);
        } else {
            actualLength = countStringLength(tableName, column, stringValue, dialect);
        }
        if (actualLength <= maxLength) {
            return fieldValue;
        }

        String columnName = column.getColumnName();
        String details = String.format("Value too long for %s.%s, actual length: %s, max length: %s, value: %s",
            tableName, columnName, actualLength, maxLength, StringTools.ellipsis(stringValue, 50));
        if (validStrategy == ValidStrategy.THROW) { // 抛出异常
            throw new ServiceException(DbErrorCode.DB_COLUMN_VALUE_TOO_LONG, details);
        } else if (validStrategy == ValidStrategy.TRANSFORM) { // 截短
            String truncatedValue;
            if (column.isSupportUnicode()) {
                truncatedValue = truncateUnicode(tableName, column, maxLength, stringValue, dialect);
            } else {
                truncatedValue = truncateString(tableName, column, maxLength, stringValue, dialect);
            }
            log.info(details + ", truncated value: " + StringTools.ellipsis(truncatedValue, 50));
            return truncatedValue;
        } else { // 警告
            log.warn(details);
            return fieldValue;
        }
    }

    /** 计算字符串长度 **/
    protected int countUnicodeLength(String tableName, SimpleFieldColumn column, String fieldValue,
            SqlDialect dialect) {
        return fieldValue.length();
    }

    /** 截短字符串 **/
    protected String truncateUnicode(String tableName, SimpleFieldColumn column, int maxLength, String fieldValue,
            SqlDialect dialect) {
        return fieldValue.substring(0, maxLength);
    }

    /** 计算字符串长度 **/
    protected int countStringLength(String tableName, SimpleFieldColumn column, String fieldValue, SqlDialect dialect) {
        Map<Character, Integer> charLengthMaps = getCharLengthMaps(tableName, column, dialect);
        int count = 0;
        for (int i = 0, length = fieldValue.length(); i < length; i++) {
            char c = fieldValue.charAt(i);
            if (c <= 0x7F) {
                count += charLengthMaps.get('A');
            } else if (c <= 0x7FF) {
                count += charLengthMaps.get('B');
            } else if (Character.isHighSurrogate(c)) {
                count += charLengthMaps.get('D');
                i++;
            } else {
                count += charLengthMaps.get('C');
            }
        }
        return count;
    }

    /** 截短字符串 **/
    protected String truncateString(String tableName, SimpleFieldColumn column, int maxLength, String fieldValue,
            SqlDialect dialect) {
        Map<Character, Integer> charLengthMaps = getCharLengthMaps(tableName, column, dialect);
        StringBuilder buffer = new StringBuilder();
        int count = 0;
        for (int i = 0, length = fieldValue.length(); i < length; i++) {
            char c = fieldValue.charAt(i);
            if (c <= 0x7F) {
                count += charLengthMaps.get('A');
            } else if (c <= 0x7FF) {
                count += charLengthMaps.get('B');
            } else if (Character.isHighSurrogate(c)) {
                count += charLengthMaps.get('D');
            } else {
                count += charLengthMaps.get('C');
            }
            if (count <= maxLength) {
                buffer.append(c);
                if (Character.isHighSurrogate(c)) {
                    i++;
                    if (i < fieldValue.length()) {
                        buffer.append(fieldValue.charAt(i));
                    }
                }
            } else {
                return buffer.toString();
            }
        }
        return fieldValue;
    }

    /**
     * 获取默认验证策略<br>
     * 注意: 由于@Column的length/precision/scale有默认值, 一旦启用, 则所有字段都需要覆盖设置!
     * 
     * @return ValidStrategy
     */
    public ValidStrategy getDefaultValidStrategy() {
        return defaultValidStrategy;
    }

    /**
     * 设置默认验证策略<br>
     * 注意: 由于@Column的length/precision/scale有默认值, 一旦启用, 则所有字段都需要覆盖设置!
     */
    public void setDefaultValidStrategy(ValidStrategy defaultValidStrategy) {
        if (defaultValidStrategy == null || defaultValidStrategy == ValidStrategy.AUTO) {
            this.defaultValidStrategy = ValidStrategy.NONE;
        } else {
            this.defaultValidStrategy = defaultValidStrategy;
        }
    }

    @Override
    public void setPlugins(DbPluginHelper plugins) {
        this.plugins = plugins;
    }
}
